package edu.udo.sopra10.chaturaji.entities;

import static edu.udo.sopra10.chaturaji.entities.Player.Type.AI_EASY;
import static edu.udo.sopra10.chaturaji.entities.Player.Type.AI_HARD;
import static edu.udo.sopra10.chaturaji.entities.Player.Type.AI_MEDIUM;
import static edu.udo.sopra10.chaturaji.util.ExceptionUtil.assertInRange;
import static edu.udo.sopra10.chaturaji.util.ExceptionUtil.assertNotNull;
import static edu.udo.sopra10.chaturaji.util.ExceptionUtil.assertNotNullOrContainsNull;
import static java.lang.Integer.MAX_VALUE;
import static java.util.Collections.singleton;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableSet;
import static org.apache.commons.lang.builder.ToStringStyle.SHORT_PREFIX_STYLE;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.builder.ToStringBuilder;

import edu.udo.sopra10.chaturaji.entities.pieces.King;
import edu.udo.sopra10.chaturaji.entities.pieces.Piece;

/**
 * A player in a single game. This entity encapsulates player data as well as
 * score tracking.
 * 
 * @author Simon Dierl
 */
public final class Player {
	/**
	 * A player type, representing the input method used etc.
	 * 
	 * @author Simon Dierl
	 */
	public static enum Type {
		/**
		 * The easy AI.
		 */
		AI_EASY,
		/**
		 * The AI with the highest possible difficulty.
		 */
		AI_HARD,
		/**
		 * The moderately difficult AI.
		 */
		AI_MEDIUM,
		/**
		 * A human player that plays by mail.
		 */
		EMAIL,
		/**
		 * A human player that makes her input into the GUI (Hotseat).
		 */
		HUMAN;
	}

	/**
	 * The check-list for assassinations of opposing kings
	 */
	private final Set<Byte> assassinationChecklist;

	/**
	 * All pieces captured by this player in the course of the game.
	 */
	private final Collection<Piece> capturedPieces = new LinkedList<Piece>();

	/**
	 * The {@link Game} in which the player plays.
	 */
	private Game game;

	/**
	 * Get if the user has used unfair tricks, such as loading. This disables
	 * highscore recording.
	 * 
	 * @return if the game was tainted.
	 */
	public boolean isTainted() {
		return taint;
	}
	
	/**
	 * Taint the game because the user has used tricks.
	 */
	public void taint() {
		taint = true;
	}
	
	/**
	 * The player's name, or, if she is an email player, her RFC822-compatible
	 * mail address.
	 */
	private String name;

	/**
	 * The pieces owned by this player.
	 */
	private final Collection<Piece> pieces = new ArrayList<Piece>(8);

	/**
	 * The score accumulated by the player in this game.
	 */
	private int score;

	/**
	 * If the player is tainted, i.e. the the game was loaded, the player used
	 * hints etc.
	 */
	private boolean taint = false;

	/**
	 * The player's position in the turn order.
	 */
	private byte turnOrderPosition;

	/**
	 * The turns made by this player, in chronological order.
	 */
	private final List<Turn> turns = new LinkedList<Turn>();

	/**
	 * The {@link Type} of this player.
	 */
	private Type type;

	/**
	 * Create a new player with the given name, {@link Type} and {@link Color}.
	 * 
	 * @param game
	 *            the game which the player plays, must not be {@code null}.
	 * @param name
	 *            the name of the player or, if she plays by mail, her
	 *            RFC822-compatible mail address. {@code null} is interpreted as
	 *            {@code ""}.
	 * @param type
	 *            the player's type. Must not be {@code null}.
	 * @param turnOrderPosition
	 *            the player's color, also determines his position in the move
	 *            order, must not be in {@code [0, 3]}.
	 */
	public Player(final Game game, final String name, final Type type,
			final byte turnOrderPosition) {
		setGame(game);
		setName(name);
		setType(type);
		setTurnOrderPosition(turnOrderPosition);
		assassinationChecklist = new HashSet<Byte>();
		assassinationChecklist.add(turnOrderPosition);
	}

	/**
	 * Add an assassinated {@link King} to the collection of kings assassinated
	 * by the {@link Player}.
	 * 
	 * @param owner
	 *            the new assassinated king's owner's turn order position, must
	 *            be in {@code [0, 3]}.
	 * @return if the king was new to the collection.
	 */
	public boolean addAssassinatedKing(final byte owner) {
		return assassinationChecklist.add(assertInRange(owner, (byte) 0,
				(byte) 3, "owner"));
	}

	/**
	 * Add several assassinated {@link King}s to the collection of kings
	 * assassinated by the {@link Player}.
	 * 
	 * @param owners
	 *            the new assassinated kings' owners' turn order positions, must
	 *            not be {@code null} or contain {code null} and be in
	 *            {@code [0, 3]}. This collection is copied to preserve
	 *            encapsulation.
	 * @return if one if the kings was new to the collection.
	 */
	public boolean addAssassinatedKings(final Collection<Byte> owners) {
		boolean ret = false;
		for (final byte owner : assertNotNullOrContainsNull(owners, "owners")) {
			ret |= addAssassinatedKing(owner);
		}
		return ret;
	}

	/**
	 * Add a captured {@link Piece} to the collection of pieces captured by the
	 * {@link Player}.
	 * 
	 * @param capturedPiece
	 *            the new captured piece, must not be {@code null}.
	 * @return if the piece was new to the collection. Always returns
	 *         {@code true}.
	 */
	public boolean addCapturedPiece(final Piece capturedPiece) {
		return capturedPieces
				.add(assertNotNull(capturedPiece, "captured piece"));
	}

	/**
	 * Add several captured {@link Piece}s to the collection of pieces captured
	 * by the {@link Player}.
	 * 
	 * @param capturedPieces
	 *            the new captured pieces, must not be {@code null} or contain
	 *            {code null}. This collection is copied to preserve
	 *            encapsulation.
	 * @return if one if the pieces was new to the collection. Always returns
	 *         {@code !capturedPieces.isEmpty()}.
	 */
	public boolean addCapturedPieces(final Collection<Piece> capturedPieces) {
		return this.capturedPieces.addAll(assertNotNullOrContainsNull(
				capturedPieces, "captured pieces"));
	}

	/**
	 * Add a {@link Piece} to the collection of pieces owned by the player.
	 * 
	 * @param piece
	 *            the new piece, must not be {@code null}.
	 * @return if the piece was new to the collection. Always returns
	 *         {@code true}.
	 */
	public boolean addPiece(final Piece piece) {
		return pieces.add(assertNotNull(piece, "piece"));
	}

	/**
	 * Add several {@link Piece}s to the collection of pieces owned by the
	 * player.
	 * 
	 * @param pieces
	 *            the new pieces, must not be {@code null} or contain {code
	 *            null}. This collection is copied to preserve encapsulation.
	 * @return if one if the pieces was new to the collection. Always returns
	 *         {@code !pieces.isEmpty()}.
	 */
	public boolean addPieces(final Collection<Piece> pieces) {
		return this.pieces
				.addAll(assertNotNullOrContainsNull(pieces, "pieces"));
	}

	/**
	 * Append a {@link Turn} to the list of turns made by the player. The turn
	 * must be the newest turn made by this player.
	 * 
	 * @param turn
	 *            the new turn, must not be {@code null}.
	 * @return if the turn was new to the list. Always returns {@code true}.
	 */
	public boolean addTurn(final Turn turn) {
		return turns.add(assertNotNull(turn, "turn"));
	}

	/**
	 * Append several {@link Turn}s to the list of turns made by the player. The
	 * turns must be the newest turns made by their player and their internal
	 * ordering must also be chronological.
	 * 
	 * @param turns
	 *            the new turns, must not be {@code null} or contain {code
	 *            null}. This list is copied to preserve encapsulation.
	 * @return if one if the turns was new to the list. Always returns
	 *         {@code !turns.isEmpty()}.
	 */
	public boolean addTurns(final List<Turn> turns) {
		return this.turns.addAll(assertNotNullOrContainsNull(turns, "turns"));
	}

	/**
	 * Modify the player's score by a given value.
	 * 
	 * @param score
	 *            the modification to add to the score. May be negative to
	 *            decrement the score, but must not reduce the score to a value
	 *            {@code < 0}.
	 * @return the new score.
	 */
	public int alterScore(final int score) {
		setScore(this.score + score);
		return this.score;
	}

	/**
	 * Remove all assassinated {@link King}s from the collection of kings
	 * assassinated by the {@link Player}.
	 */
	public void clearAssassinatedKings() {
		assassinationChecklist.retainAll(singleton(turnOrderPosition));
	}

	/**
	 * Remove all captured {@link Piece}s from the collection of pieces captured
	 * by the {@link Player}.
	 */
	public void clearCapturedPieces() {
		capturedPieces.clear();
	}

	/**
	 * Remove all {@link Piece}s from the collection of pieces owned by the
	 * {@link Player}.
	 */
	public void clearPieces() {
		pieces.clear();
	}

	/**
	 * Remove all {@link Turn}s from the list of turns made by the
	 * {@link Player}.
	 */
	public void clearTurns() {
		turns.clear();
	}

	/**
	 * Get the set of {@link Byte}s (turn order positions) for which
	 * assassinated kings were already registered, including the player's
	 * position.
	 * 
	 * @return the assassinated positions plus this player's position, never
	 *         {@code null}, never contains {@code null}, always unmodifiable
	 *         and in {@code [0, 3]}.
	 */
	public Set<Byte> getAssassinationChecklist() {
		return unmodifiableSet(assassinationChecklist);
	}

	/**
	 * Get a collection of all {@link Piece}s captured by the player in this
	 * game.
	 * 
	 * @return all pieces captured by this player. Always unmodifiable and never
	 *         {@code null}, never contains {@code null}.
	 */
	public Collection<Piece> getCapturedPieces() {
		return unmodifiableCollection(capturedPieces);
	}

	/**
	 * Get the {@link Game} in which the player plays.
	 * 
	 * @return the player's game, never {@code null}.
	 */
	public Game getGame() {
		return game;
	}

	/**
	 * Get the player's name.
	 * 
	 * @return the player's name, or, if she plays by mail, her
	 *         RFC822-compatible mail address. Never {@code null}.
	 */
	public String getName() {
		return name;
	}

	/**
	 * Get a collection of all {@link Piece}s owned by the player.
	 * 
	 * @return all pieces owned by the player. Never {@code null}, never
	 *         contains {@code null} , always unmodifiable.
	 */
	public Collection<Piece> getPieces() {
		return unmodifiableCollection(pieces);
	}

	/**
	 * Get the score accumulated by this player in the game.
	 * 
	 * @return the player's current score. Always {@code >= 0}.
	 */
	public int getScore() {
		return score;
	}

	/**
	 * Get the position of the player in the order of turns
	 * 
	 * @return the player's color, always in {@code [0, 3]}.
	 */
	public byte getTurnOrderPosition() {
		return turnOrderPosition;
	}

	/**
	 * Get a chronological list of all {@link Turn}s owned by the player.
	 * 
	 * @return all turns made by the player, in chronological order. Never
	 *         {@code null}, never contains {@code null}, always unmodifiable.
	 */
	public List<Turn> getTurns() {
		return unmodifiableList(turns);
	}

	/**
	 * Get the {@link Type} of player.
	 * 
	 * @return the player's type. Never {@code null}.
	 */
	public Type getType() {
		return type;
	}

	/**
	 * Remove an assassinated {@link King} from the collection of kings
	 * assassinated by the {@link Player}.
	 * 
	 * @param owner
	 *            the turn order position of the owner of the assassinated king
	 *            to remove, must be in {@code [0, 3]}.
	 * @return if the king was contained in the collection.
	 */
	public boolean removeAssassinatedKing(final byte owner) {
		return assassinationChecklist.remove(assertInRange(owner, (byte) 0,
				(byte) 3, "owner"));
	}

	/**
	 * Remove several assassinated {@link King}s from the collection of kings
	 * assassinated by the {@link Player}.
	 * 
	 * @param owners
	 *            the turn order positions of the owners of the assassinated
	 *            kings to remove, must not be {@code null} or contain
	 *            {@code null} and be in {@code [0, 3]}.
	 * @return if any of the kings were contained in the collection.
	 */
	public boolean removeAssassinatedKings(final Collection<Byte> owners) {
		boolean ret = false;
		for (final byte owner : assertNotNullOrContainsNull(owners, "owners")) {
			ret |= removeAssassinatedKing(owner);
		}
		return ret;
	}

	/**
	 * Remove a captured {@link Piece} from the collection of pieces captured by
	 * the {@link Player}.
	 * 
	 * @param capturedPiece
	 *            the captured piece to remove, must not be {@code null}.
	 * @return if the piece was contained in the collection.
	 */
	public boolean removeCapturedPiece(final Piece capturedPiece) {
		return capturedPieces.remove(assertNotNull(capturedPiece,
				"captured piece"));
	}

	/**
	 * Remove several captured {@link Piece}s from the collection of pieces
	 * captured by the {@link Player}.
	 * 
	 * @param capturedPieces
	 *            the captured pieces to remove, must not be {@code null}. May
	 *            contain {@code null}, although this has no effect and is
	 *            discouraged.
	 * @return if any of the pieces were contained in the collection.
	 */
	public boolean removeCapturedPieces(final Collection<Piece> capturedPieces) {
		return this.capturedPieces.removeAll(assertNotNull(capturedPieces,
				"captured pieces"));
	}

	/**
	 * Remove a {@link Piece} from the collection of pieces owned by the player.
	 * 
	 * @param piece
	 *            the piece to remove, must not be {@code null}.
	 * @return if the piece was contained in the collection.
	 */
	public boolean removePiece(final Piece piece) {
		return pieces.remove(assertNotNull(piece, "piece"));
	}

	/**
	 * Remove several {@link Piece}s from the collection of pieces owned by the
	 * player.
	 * 
	 * @param pieces
	 *            the pieces to remove, must not be {@code null}. May contain
	 *            {@code null}, although this has no effect and is discouraged.
	 * @return if any of the pieces were contained in the collection.
	 */
	public boolean removePieces(final Collection<Piece> pieces) {
		return this.pieces.removeAll(assertNotNull(pieces, "pieces"));
	}

	/**
	 * Remove a {@link Turn} from the list of turns made by the player.
	 * 
	 * @param turn
	 *            the turn to remove, must not be {@code null}.
	 * @return if the turn was contained in the list.
	 */
	public boolean removeTurn(final Turn turn) {
		return turns.remove(assertNotNull(turn, "turn"));
	}

	/**
	 * Remove several {@link Turn}s from the list of turns made by the player.
	 * 
	 * @param turns
	 *            the turns to remove, must not be {@code null}. May contain
	 *            {@code null}, although this has no effect and is discouraged.
	 * @return if any of the turns were contained in the list.
	 */
	public boolean removeTurns(final Collection<Turn> turns) {
		return this.turns.removeAll(assertNotNull(turns, "turns"));
	}

	/**
	 * Set the {@link Game} in which the player plays.
	 * 
	 * @param game
	 *            the player's new game, must not be {@code null}.
	 */
	public void setGame(final Game game) {
		this.game = assertNotNull(game, "game");
	}

	/**
	 * Set the player's name.
	 * 
	 * @param name
	 *            the new name of the player or, if she plays by mail, her
	 *            RFC822-compatible mail address. {@code null} is interpreted as
	 *            {@code ""}.
	 */
	public void setName(final String name) {
		this.name = name == null ? "" : name;
	}

	/**
	 * Set the player's score.
	 * 
	 * @param score
	 *            the player's new score. Must be {@code >= 0}.
	 */
	public void setScore(final int score) {
		this.score = assertInRange(score, 0, MAX_VALUE, "score");
	}

	/**
	 * Set the player's position in the order of turns. Use of this method is
	 * discouraged to avoid disturbance of a running game.
	 * 
	 * @param turnOrderPosition
	 *            the player's new color, must not be in {@code [0, 3]}.
	 */
	public void setTurnOrderPosition(final byte turnOrderPosition) {
		this.turnOrderPosition = assertInRange(turnOrderPosition, (byte) 0,
				(byte) 3, "turn order position");
	}

	/**
	 * Set the player's {@link Type}.
	 * 
	 * @param type
	 *            the new type of the player. Must not be {@code null}.
	 */
	public void setType(final Type type) {
		this.type = assertNotNull(type, "type");
		taint = (type == AI_EASY || type == AI_MEDIUM || type == AI_HARD);
	}

	@Override
	public String toString() {
		return new ToStringBuilder(this, SHORT_PREFIX_STYLE)
				.append("name", name)
				.append("turnOrderPosition", turnOrderPosition)
				.append("type", type).append("score", score).toString();
	}

	/**
	 * Remove all {@link Turn}s above a certain turn number from the list of
	 * turns made by the player.
	 * 
	 * @param from
	 *            the lowest position to remove.
	 */
	public void truncateHistory(final int from) {
		turns.subList(from, turns.size()).clear();
	}
}
